<?php

namespace Yolo\Service;

use Hyperf\Support\Filesystem\Filesystem;
use ReflectionAttribute;
use ReflectionClass;
use ReflectionException;
use ReflectionMethod;
use Symfony\Component\Console\Output\ConsoleOutput;
use Yolo\Commands\CommandRegistry;
use Yolo\ComponentManager\ComponentManager;
use Yolo\Components\ApplicationRegistry\ApplicationRegistryComponent;
use Yolo\Components\EventDispatcher\Annotations\EventDispatcher;
use Yolo\Components\EventDispatcher\EventDispatcherComponent;
use Yolo\Components\Middleware\MiddlewareComponent;
use Yolo\Components\Middleware\MiddlewareManager;
use Yolo\Components\RpcClient\RpcClientComponent;
use Yolo\Components\Scheduler\Annotations\Scheduler;
use Yolo\Components\Scheduler\SchedulerComponent;
use Yolo\Components\Scheduler\SchedulerInterface;
use Yolo\Components\Scheduler\SchedulerManager;
use Yolo\Components\Validator\ValidatorComponent;
use Yolo\Components\Validator\ValidatorFactory;
use Yolo\Config\Annotations\UseConfig;
use Yolo\Di\Annotations\Singleton;
use Yolo\Di\DI;
use Yolo\Di\Errors\CircularDependencyException;
use Yolo\Di\Errors\InvalidAttributeException;
use Yolo\Di\Errors\ParameterTypeEmptyException;
use Yolo\Errors\AppEntryNotFoundException;
use Yolo\Logs\LogComponent;
use Yolo\Rpc\Annotations\AppScheme;
use Yolo\Rpc\Annotations\RpcController;
use Yolo\Utils\Console;
use Yolo\Utils\Str;

#[Singleton]
class ApplicationContainer
{
    /**
     * @var string $version The version of the framework.
     */
    const VERSION = '2.0.1';

    /**
     * @var string $appDir The application directory.
     */
    private string $appDir;

    /**
     * @var string $baseNamespace The base namespace of the application.
     */
    private string $baseNamespace;

    /**
     * @var array $customScanDir The custom scan directory of the application.
     */
    private array $customScanDir;

    private \Yolo\Config\Config $config;

    /**
     * Entry point of the application.
     * @var AbstractService
     */
    protected AbstractService $app;

    /**
     * App scheme of the application.
     * @var AppScheme
     */
    protected AppScheme $appScheme;

    /**
     * @var bool $booted Whether the application is booted.
     */
    protected bool $booted = false;

    public function __construct(private readonly RpcApiManager $rpcApiManager)
    {
    }

    /**
     * Init the application.
     * @return void
     */
    public function init(): void
    {
        $this->sayHello();

        $this->config = new \Yolo\Config\Config(BASE_PATH);

        $this->addInjections();
    }

    /**
     * Run the application.
     * @param string $entry
     * @return void
     * @throws CircularDependencyException
     * @throws InvalidAttributeException
     * @throws ParameterTypeEmptyException
     * @throws ReflectionException|AppEntryNotFoundException
     */
    public function run(string $entry): void
    {
        $this->init();

        $this->app = new $entry($this);

        $this->handleEntry();

        $this->loadComponents();

        $this->loadMiddlewares();

        $this->loadPreProcessors();

        $this->appDir = realpath(BASE_PATH . '/app');

        $this->baseNamespace = 'App';

        $annotations = include BASE_PATH . '/config/autoload/annotations.php';
        $customScanDir = $annotations['yolo']['scan'] ?? [];
        if (is_array($customScanDir)) {
            $this->customScanDir = $customScanDir;
        }

        $this->load();

        $this->app->main();

        $this->booted = true;
    }

    public function getApp(): AbstractService
    {
        return $this->app;
    }

    /**
     * Get the application scheme.
     * @return AppScheme
     */
    public function getAppScheme(): AppScheme
    {
        return $this->appScheme;
    }

    /**
     * The booted status of the application.
     * @return bool
     */
    public function isBooted(): bool
    {
        return $this->booted;
    }

    /**
     * @return void
     * @throws CircularDependencyException
     * @throws ParameterTypeEmptyException
     * @throws ReflectionException|InvalidAttributeException
     */
    public function load(): void
    {
        $files = $this->getAppFiles($this->baseNamespace, $this->appDir);
        foreach ($this->customScanDir as $customScanDir) {
            $files = array_merge($files, $this->getAppFiles($customScanDir['baseNamespace'], $customScanDir['path']));
        }

        foreach ($files as $file) {

            if (str_ends_with($file['className'], 'Interface')) {
                continue;
            }

            if (str_starts_with($file['className'], 'Abstract')) {
                continue;
            }

            $fullFilePath = $file['path'] . DIRECTORY_SEPARATOR . $file['className'] . '.php';
            $class = $file['namespace'] . '\\' . $file['className'];

            if (!class_exists($class)) {

                Console::log("File $fullFilePath is not a psr-4 standard class file, ignored.", 'warn');
                continue;
            }

            $reflection = $this->createReflection($class);

            // Ignore abstract class and interface.
            if ($reflection->isInterface() || $reflection->isAbstract()) {
                continue;
            }

            $attributes = $reflection->getAttributes();
            foreach ($attributes as $attribute) {

                $this->handleClassAttribute($reflection, $attribute);
            }

            $methods = $reflection->getMethods(ReflectionMethod::IS_PUBLIC);
            foreach ($methods as $method) {

                $attributes = $method->getAttributes();
                foreach ($attributes as $attribute) {

                    $this->handleMethodAttribute($reflection, $method, $attribute);
                }
            }
        }
    }

    /**
     * 处理方法的注解
     * @param ReflectionClass $reflection
     * @param ReflectionMethod $method
     * @param ReflectionAttribute $attribute
     * @return void
     * @throws CircularDependencyException
     * @throws InvalidAttributeException
     * @throws ParameterTypeEmptyException
     * @throws ReflectionException
     */
    private function handleMethodAttribute(
        ReflectionClass $reflection,
        ReflectionMethod $method,
        ReflectionAttribute $attribute
    ): void {
        $name = $attribute->getName();
        switch ($name) {
            case EventDispatcher::class:
                $instance = $attribute->newInstance();

                $componentManager = DI::use(ComponentManager::class);
                $eventDispatcher = $componentManager->getRegisteredComponent(EventDispatcherComponent::class);
                if (!$eventDispatcher) {
                    Console::log("EventDispatcher component is not enabled, ignored.", 'warning');
                    return;
                }

                $eventDispatcher->addEvent('event', $instance->getTopic(), $instance->getEndpoint(), [$reflection->getName(), $method->getName()]);
                break;

            default:
                break;
        }
    }

    private function getAppFiles(string $namespace, string $dir): array
    {
        $allFiles = [];

        $fileSystem = new Filesystem();
        $files = $fileSystem->files($dir);
        $dirs = $fileSystem->directories($dir);

        foreach ($files as $file) {

            if ($file->getExtension() !== 'php') {
                continue;
            }

            $allFiles[] = [
                'namespace' => $namespace,
                'path' => $dir,
                'className' => $file->getFilenameWithoutExtension(),
            ];
        }

        foreach ($dirs as $subDir) {

            $dirNamespace = $namespace . '\\' . Str::getLast($subDir, DIRECTORY_SEPARATOR);
            $allFiles = array_merge($allFiles, $this->getAppFiles($dirNamespace, $subDir));
        }

        return $allFiles;
    }

    /**
     * @throws ReflectionException
     */
    private function createReflection(string $class): ReflectionClass
    {
        return new ReflectionClass($class);
    }

    /**
     * @param ReflectionClass $reflection
     * @param ReflectionAttribute $attribute
     * @return void
     * @throws CircularDependencyException
     * @throws ParameterTypeEmptyException
     * @throws ReflectionException|InvalidAttributeException
     */
    private function handleClassAttribute(
        ReflectionClass $reflection,
        ReflectionAttribute $attribute
    ): void {
        $name = $attribute->getName();
        $parameters = $attribute->getArguments();
        switch ($name) {
            case RpcController::class:
                $this->rpcApiManager->getRpcApi($this->appScheme, $reflection);
                DI::instance($reflection->getName(), DI::use($reflection->getName()));
                break;

            case Singleton::class:
                DI::use($reflection->getName());
                break;

            case Scheduler::class:
                $this->registerScheduler($reflection->getName(), $parameters);
                break;

            default:
                break;

        }
    }

    /**
     * Get config.
     * @return \Yolo\Config\Config
     */
    public function getConfig(): \Yolo\Config\Config
    {
        return $this->config;
    }

    /**
     * @return void
     * @throws AppEntryNotFoundException
     */
    private function handleEntry(): void
    {
        $reflection = new ReflectionClass($this->app);
        $appEntry = array_filter($reflection->getAttributes(AppScheme::class), function ($attribute) {
            return $attribute->getName() === AppScheme::class;
        });

        if (!$appEntry) {
            throw new AppEntryNotFoundException();
        }

        foreach ($reflection->getAttributes() as $attribute) {

            if ($attribute->getName() !== AppScheme::class) {
                continue;
            }

            $this->appScheme = $attribute->newInstance();
        }
    }

    /**
     * Register scheduler
     * @param string $scheduler
     * @param array $parameters
     * @return void
     * @throws CircularDependencyException
     * @throws InvalidAttributeException
     * @throws ParameterTypeEmptyException
     * @throws ReflectionException
     */
    private function registerScheduler(string $scheduler, array $parameters): void
    {
        $componentManager = DI::use(ComponentManager::class);

        // 计划任务组件未开启
        if (!$componentManager->componentEnabled(SchedulerComponent::class)) {
            return;
        }

        $schedulerManager = DI::use(SchedulerManager::class);

        $config = $this->getConfig()->get("scheduler.$parameters[0]");
        if (!$config) {
            Console::log("The configuration of the scheduler($scheduler) is not found.", 'warning');
            return;
        }

        $validationFactory = DI::use(ValidatorFactory::class);
        $messageBag = $validationFactory->validate('@schedulerConfigValidation', $config);
        if ($messageBag) {

            Console::log("Invalid config value of the scheduler($scheduler).", 'warning');
            return;
        }

        $schedulerInstance = new $scheduler($config);
        if (!$schedulerInstance instanceof SchedulerInterface) {
            Console::log("The scheduler class must implement the SchedulerInterface interface.", 'warn');
            return;
        }

        $schedulerManager->addScheduler($schedulerInstance);
    }

    /**
     * Add injections
     * @return void
     */
    private function addInjections(): void
    {
        // 添加配置注解解析
        DI::addCustomPropertyAttribute(UseConfig::class);
    }

    /**
     * Load components.
     * @return void
     * @throws ReflectionException
     * @throws CircularDependencyException
     * @throws ParameterTypeEmptyException|InvalidAttributeException
     */
    private function loadComponents(): void
    {
        $componentManager = DI::use(ComponentManager::class);

        $components = array_merge(
            $this->getComponents(),
            $this->app->components()
        );

        foreach ($components as $component) {
            $componentManager->registerComponent($component);
        }

        $componentManager->runAfterRegistered();
    }

    /**
     * Load middlewares.
     * @return void
     * @throws CircularDependencyException
     * @throws InvalidAttributeException
     * @throws ParameterTypeEmptyException
     * @throws ReflectionException
     */
    private function loadMiddlewares(): void
    {
        $middlewareManager = DI::use(MiddlewareManager::class);
        $middlewares = array_merge(
            $this->middlewares(),
            $this->app->middlewares()
        );

        foreach ($middlewares as $middleware) {
            $middlewareManager->register($middleware);
        }
    }

    /**
     * Load pre-processors.
     * @return void
     * @throws CircularDependencyException
     * @throws InvalidAttributeException
     * @throws ParameterTypeEmptyException
     * @throws ReflectionException
     */
    private function loadPreProcessors(): void
    {
        $preprocessorRegistry = DI::use(PreProcessorRegistry::class);
        foreach ($this->app->preProcessors() as $name => $processor) {
            $preprocessorRegistry->register($name, $processor);
        }
    }

    /**
     * Get commands.
     * @return array
     * @throws CircularDependencyException
     * @throws InvalidAttributeException
     * @throws ParameterTypeEmptyException
     * @throws ReflectionException
     */
    public function getCommands(): array
    {
        $commandRegistry = DI::use(CommandRegistry::class, false);
        return $commandRegistry->commands();
    }

    /**
     * Get components.
     * @return string[]
     */
    private function getComponents(): array
    {
        return [
            LogComponent::class,
            ValidatorComponent::class,
            ApplicationRegistryComponent::class,
            RpcClientComponent::class,
            MiddlewareComponent::class,
        ];
    }

    /**
     * Get middlewares.
     * @return string[]
     */
    private function middlewares(): array
    {
        return [
        ];
    }

    /**
     * Show the logo of the framework.
     * @return void
     */
    private function sayHello(): void
    {
        $helloMessage = sprintf('<info>%s</>', <<<TEXT

 __     __                       _         _ _                                    _ 
 \ \   / /                      | |       | (_)                                  | |
  \ \_/ /__  _   _    ___  _ __ | |_   _  | |___   _____    ___  _ __   ___ ___  | |
   \   / _ \| | | |  / _ \| '_ \| | | | | | | \ \ / / _ \  / _ \| '_ \ / __/ _ \ | |
    | | (_) | |_| | | (_) | | | | | |_| | | | |\ V /  __/ | (_) | | | | (_|  __/ |_|
    |_|\___/ \__,_|  \___/|_| |_|_|\__, | |_|_| \_/ \___|  \___/|_| |_|\___\___| (_)
                                    __/ |                                           
                                   |___/                                            

TEXT
        );
        $log = new ConsoleOutput();
        $log->writeln($helloMessage);
    }
}
